package ru.maksb.exchange.service;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.maksb.exchange.domain.MatchResult;
import ru.maksb.exchange.domain.MatchType;
import ru.maksb.exchange.domain.OpenBook;
import ru.maksb.exchange.domain.Order;
import ru.maksb.exchange.domain.OrderSide;
import ru.maksb.exchange.domain.request.CancelOrderRequest;
import ru.maksb.exchange.domain.request.NewOrderRequest;
import ru.maksb.exchange.domain.request.Request;
import ru.maksb.exchange.domain.response.Response;
import ru.maksb.exchange.domain.response.ResponseType;
import ru.maksb.exchange.server.ServerWorker;
import ru.maksb.exchange.utils.ExchangeConsts;

public class ExchangeWorker implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(ExchangeWorker.class);
    private static final String TICKETS_PROPERTIES_KEY = "exchangeTickets";
    private static final String DEFAULT_BUY_PRICE_KEY = "exchangeDefaultBuyPrice";
    private static final String DEFAULT_SELL_PRICE_KEY = "exchangeDefaultSellPrice";
    private Map<String, OpenBook> books;
    private Map<String, Order> allOrders;
    private BlockingQueue<Request> requests = new LinkedBlockingDeque<Request>();
    private AtomicBoolean exchangeWorking = new AtomicBoolean(true);
    private Map<String, ServerWorker> clientServerWorkers = new ConcurrentHashMap<String, ServerWorker>();
    private Map<String, List<Order>> clientOrders = new HashMap<String, List<Order>>();

    public ExchangeWorker(String propertiesPath) {
        logger.info("Create Exchange Worker");
        FileInputStream fis = null;
        try {
            logger.info("Load properties from {}", propertiesPath);
            fis = new FileInputStream(propertiesPath);
            System.getProperties().load(fis);
        } catch (IOException e) {
            logger.error("Cannot load properties", e);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    logger.error("cannot close FileInputStream", e);
                }
            }
        }
        books = new HashMap<String, OpenBook>();
        allOrders = new HashMap<String, Order>();
        String tickets = System.getProperty(TICKETS_PROPERTIES_KEY, "USD");
        fillBooks(tickets, books);
        logger.info("Exchange worker created");
    }
    
    private void fillBooks(String tickets, Map<String, OpenBook> books) {
        for (String ticket : tickets.split(",")) {
            logger.info("Adding book for ticket = {}", ticket);
            books.put(ticket, createBook(ticket));
        }
    }
    
    public void run() {
        while (exchangeWorking.get()) {
            try {
                Request request = requests.take();
                processRequest(request);
            } catch (InterruptedException e) {}
        }
    }
    
    private void processRequest(Request request) {
        switch (request.getRequestType()) {
        case ORDER_CANCEL:
            logger.info(request.toString());
            Response response = cancelOrder((CancelOrderRequest) request);
            processResponse(response);
            break;
        case ORDER_NEW:
            List<MatchResult> matchResultList = match((NewOrderRequest) request);
        default:
            break;
        }
    }

    private void processResponse(Response response) {
        ServerWorker worker = clientServerWorkers.get(response.getClientId());
        switch (response.getResponseType()) {
        case CANCEL_ORDER_OK:
            worker.sendMessage(ResponseFactory.cancelOrderOk(response));
            break;
        case CANCEL_ORDER_ERROR:
            worker.sendMessage(ResponseFactory.cancelOrderError(response));
            break;
        default:
            break;
        }
        
        
    }

    private OpenBook createBook(String ticket) {
        String defaultBuyPrice = System.getProperty(DEFAULT_BUY_PRICE_KEY + ticket);
        if (defaultBuyPrice == null) {
            defaultBuyPrice = System.getProperty(DEFAULT_BUY_PRICE_KEY, "0d");
        }
        String defaultSellPrice = System.getProperty(DEFAULT_SELL_PRICE_KEY + ticket);
        if (defaultSellPrice == null) {
            defaultSellPrice = System.getProperty(DEFAULT_SELL_PRICE_KEY, "0d");
        }
        OpenBook book = new OpenBook(ticket, Double.parseDouble(defaultBuyPrice),
                                             Double.parseDouble(defaultSellPrice));
        return book;
    }

    public Map<String, OpenBook> getBooks() {
        return books;
    }
    
    private Response cancelOrder(CancelOrderRequest request) {
        Order order = allOrders.get(request.getTargetOrderReference());
        if (order == null) {
            logger.info("There is no order woth refId {}", request.getTargetOrderReference());
            return new Response(ResponseType.CANCEL_ORDER_ERROR, request);
        }
        if (removeOrder(order)) {
            return new Response(ResponseType.CANCEL_ORDER_OK, request);
        } else {
            logger.info("There is no order woth refId {}", request.getTargetOrderReference());
            return new Response(ResponseType.CANCEL_ORDER_ERROR, request);
        }
    }
    
    private boolean removeOrder(Order order) {
        logger.info("Trying to remove order - {}", order);
        allOrders.remove(order.getReferenceId());
        OpenBook book = books.get(order.getTicket());
        return book.remove(order);
    }
    
    private void addOrder(Order order) {
        OpenBook book = books.get(order.getTicket());
        allOrders.put(order.getReferenceId(), order);
        book.add(order);
        logger.info("Order added - {}", order);
    }
    
    private List<MatchResult> match(NewOrderRequest request) {
        List<MatchResult> result = null;
        Order order = OrderFactory.createOrder(request);
        logger.info("New order: {}", order);
        logger.info("Try to match order");
        OpenBook book = books.get(order.getTicket());
        matchOrder(order, book);
        logger.info("Match result: {}", result);
        return result;
    }

    private List<MatchResult> matchOrder(Order order, OpenBook book) {
        Order bestOrder = null;
        List<MatchResult> matchResult = new ArrayList<MatchResult>();
        boolean isBuyOrder = order.getSide().equals(OrderSide.BUY);
        SortedSet<Order> ordersSet = isBuyOrder ? book.getSellOrders() :
                                                  book.getBuyOrders();
        do {
            if (order.getAmount() == 0) {
                break;
            }
            if (ordersSet.isEmpty()) {
                break;
            }
            bestOrder = ordersSet.first();
            if (isBuyOrder && bestOrder.getPrice() - order.getPrice() > ExchangeConsts.DELTA) {
                break;
            }
            if (!isBuyOrder && order.getPrice() - bestOrder.getPrice() > ExchangeConsts.DELTA) {
                break;
            }
            matchResult.add(matchBuySellOrders(isBuyOrder ? order : bestOrder, isBuyOrder ? bestOrder : order));
            if (bestOrder.getAmount() == 0) {
                removeOrder(bestOrder);
            }
        } while (bestOrder != null);
        if (order.getAmount() != 0)
            addOrder(order);
        return matchResult;
    }
    private double average(double d1, double d2) {
        return d1 + (d2 - d1) / 2;
    }

    
    private MatchResult matchBuySellOrders(Order buyOrder, Order sellOrder) {
        int amoundDiff = buyOrder.getAmount() - sellOrder.getAmount();
        MatchResult matchResult = new MatchResult();
        matchResult.setFirstOrderReference(buyOrder.getReferenceId());
        matchResult.setSecondOrderReference(sellOrder.getReferenceId());
        matchResult.setPrice(average(buyOrder.getPrice(), sellOrder.getPrice()));
        if (amoundDiff >= 0) {
            //новый ордер перекрыл текущий sellOrder
            matchResult.setSecondMatchType(MatchType.FILLED);
            matchResult.setAmount(sellOrder.getAmount());
            if (amoundDiff == 0) {
                matchResult.setFirstMatchType(MatchType.FILLED);
            } else {
                matchResult.setFirstMatchType(MatchType.PARTIAL);
            }
            buyOrder.setAmount(buyOrder.getAmount() - sellOrder.getAmount());
        } else {
            //новый ордер перекрыт текущим
            matchResult.setFirstMatchType(MatchType.FILLED);
            matchResult.setAmount(buyOrder.getAmount());
            if (amoundDiff == 0) {
                matchResult.setSecondMatchType(MatchType.FILLED);
            } else {
                matchResult.setSecondMatchType(MatchType.PARTIAL);
            }
            buyOrder.setAmount(0);
            sellOrder.setAmount(sellOrder.getAmount() - buyOrder.getAmount());
        } 
        return matchResult;
    }

    public BlockingQueue<Request> getRequests() {
        return requests;
    }
    
    public void addWorker(ServerWorker worker) {
        clientServerWorkers.put(worker.getClientInfo().getName(), worker);
    }
    
    public void closeWorker(String clientName) {
        clientServerWorkers.remove(clientName);
    }
}
